using System;
using Clandestine.Graphics.Window;
using System.Threading;
using System.Drawing;
using Clandestine.Base;
using System.Collections.Generic;
using Lime;

namespace Clandestine.Graphics
{
	public class Graphics
	{
		private delegate void FBODelegate();
        public IWindowHandler Window { get; private set; }
        public IWindowHandlerInternal WindowInternal { get { return (IWindowHandlerInternal)Window; } } // for outside OpenGL hackers, and internal use!
        public RenderThread Thread { get; private set; }
        public RenderList DefaultRenderList { get; set; }
        public Camera Camera { get; set; }
		public Textures Textures;
       	uint[] framebuffers = new uint[1];
		
		internal PreRenderEvent PreRender;
        
        // make sure you've blocked rendering or we'll cut you.
        private List<RenderList> renderLists = new List<RenderList>();
        public List<RenderList> RenderLists 
        {
        	get 
        	{
        		if (!((WindowHandler)Window).RenderMutex.HasMutex())
        			throw new RenderingNotBlockedException();
        		return renderLists;
        	}
        	set
        	{
        		if (!((WindowHandler)Window).RenderMutex.HasMutex())
        			throw new RenderingNotBlockedException();
        		renderLists = value; 
        	}
        }

        public int ScreenWidth;
        public int ScreenHeight;
	    
		public Graphics(WindowHandler window, int width, int height)
		{   
            this.Camera = new Camera(this);
            this.Thread = new RenderThread(this);
            this.Window = window;
            this.ScreenWidth = width;
            this.ScreenHeight = height;
			this.Textures = new Textures(this);
         	
         	this.BlockRendering();
         	
            this.RenderLists.Add(new RenderList(this));
           	this.DefaultRenderList = this.RenderLists[0];
        	
        	glSetup();
        	
        	this.UnblockRendering();
		}
		
		public void BlockRendering()
		{
			((WindowHandler)Window).RenderMutex.BlockRendering();
		}
		
		public void UnblockRendering()
		{
			((WindowHandler)Window).RenderMutex.UnblockRendering();
		}

        private void glSetup()
        {
        	WindowInternal.MakeContextCurrent();
        	
            // Put projection matrix in editing slot, reset it, and apply an orthographic transformation.
            // (Note: Don't fuss about the -1000...1000 z coord limits - we do all our depth sorting in software b/c 
            //   you fucking have to to make alpha-blending look anything other than shit)
            GL.MatrixMode(MatrixMode.Projection);
            GL.LoadIdentity();
            GL.Ortho(0, ScreenWidth, ScreenHeight, 0, -1000, 1000); // (0,0) is top-left, (width,height) is bottom-right.

            GL.Enable(EnableCap.Blend); // ALPHA-BLENDING.
            
            WindowInternal.ReleaseContext();
        }
        
        public void Resize(int screenWidth, int screenHeight)
        {
			this.BlockRendering();
        	WindowInternal.MakeContextCurrent();
        
        	this.ScreenWidth = screenWidth;
			this.ScreenHeight = screenHeight;
        			
            GL.MatrixMode(MatrixMode.Projection);
            GL.LoadIdentity();
            GL.Ortho(0, ScreenWidth, ScreenHeight, 0, -1000, 1000); // (0,0) is top-left, (width,height) is bottom-right.
			GL.Viewport(0, 0, ScreenWidth, ScreenHeight);
			
			WindowInternal.ReleaseContext();
			this.UnblockRendering();
        }
		
		public void ForceRender()
		{
			Render();
		}

        // Called by RenderThread.renderThreadProc()
        internal void Render()
        {
            this.BlockRendering();

            WindowInternal.MakeContextCurrent();

            // Rendering here. _totally_.
            // 1. CLEAR EVERYTHING.
            GL.ClearColor(Camera.BackgroundColor.R, Camera.BackgroundColor.G, Camera.BackgroundColor.B, Camera.BackgroundColor.A);
            GL.Clear((uint)ClearBufferMask.ColorBufferBit); // TODO: investigate bad binding!

            // 2. CAMERA.
            Camera.SetMatrix();
			
			// 3a. Pre-render pass...
			if (PreRender != null)
				PreRender();

            // 3b. RENDERPOCALYPSE.
        	foreach (RenderList list in RenderLists)
                foreach (Renderable r in list.Values)
                    r.Render();

            WindowInternal.SwapBuffers(); // No longer outside the lock.  Seems we need the context current to swap buffers on Win...
            WindowInternal.ProcessEvents();

            WindowInternal.ReleaseContext();

            this.UnblockRendering();
        }
        
        public Texture RenderToTexture(RenderList r, Size size, Texture tex, bool keepAlive = false)
        {	
        	if (!(tex is FramebufferTexture))
        		throw new ArgumentException("Should be a FramebufferTexture rly...");
			
			
			
        	return RenderToTexture(r, size, tex.Identifier, keepAlive);
        }
        
        public void RenderToFile(RenderList r, Size size, string filename)
        {
        	DateTime dtStart = DateTime.Now;
			
			// Create texture, and render to it. Pass a delegate to do the saving
			string guid = Guid.NewGuid().ToString();
			Texture texture = this.Textures.AddBlank(guid, size, false, false);
			this.renderFBO(r, size, texture, delegate {
	            // Get datas
	            IntPtr data = IntPtr.Zero;
				IntPtr RAMS = System.Runtime.InteropServices.Marshal.AllocHGlobal(size.Width * size.Height * 4); // 4 b/c 32bpp
				GL.ReadPixels(0, 0, size.Width, size.Height, (PixelFormat)GLEnum.Bgra, PixelType.UnsignedByte, RAMS);
	            data = RAMS;
	            
	            // Save to disk
	            Bitmap bmp = new Bitmap(size.Width, size.Height, 4 * size.Width, System.Drawing.Imaging.PixelFormat.Format32bppArgb, data);
	            bmp.Save(filename);
	            bmp.Dispose();
				
				// Essential: deallocate mem.
				System.Runtime.InteropServices.Marshal.FreeHGlobal(RAMS); 
			});
			
			// Don't forget to delete our texture!
			this.Textures.Remove(guid);
        	
        	Log.i("Render to file '{0}' took {1}ms", filename, (DateTime.Now - dtStart).TotalMilliseconds);
        }
        
        public Texture RenderToTexture(RenderList r, Size size, string textureIdentifier, bool keepAlive = false)
        {
			DateTime dtStart = DateTime.Now;
			
			// Create texture, and render to it
			Texture texture = this.Textures.AddBlank(textureIdentifier, size, false, keepAlive);
        	this.renderFBO(r, size, texture, null);
        	
        	Log.i("Render to texture '{0}' took {1}ms", textureIdentifier, (DateTime.Now - dtStart).TotalMilliseconds);
        	return texture;
        }
        
        private void renderFBO(RenderList r, Size size, Texture texture, FBODelegate useFBOFunc)
        {
        	this.BlockRendering();
			WindowInternal.MakeContextCurrent();
        	
			// Setup framebuffer
          	uint framebuffer;
          	if (!GL.IsFramebuffer(framebuffers[0]))
          		GL.GenFramebuffers(1, framebuffers);
			framebuffer = framebuffers[0];
        	GL.BindFramebuffer(GLEnum.Framebuffer, framebuffer);

			// Bind texture
			GL.BindTexture(TextureTarget.Texture2D, 0); // just incase
        	GL.FramebufferTexture2D(GLEnum.Framebuffer, GLEnum.ColorAttachment0, GLEnum.Texture2D, texture.TexturePointer, 0);

			// Drawbuffer!
			GL.DrawBuffer((DrawBufferMode)GLEnum.ColorAttachment0);
			
        	// Okay. I really don't understand OGL co-ordinates. I've fucked around,
        	// and this finally works. FINALLY.
        	
        	// Ortho stuffs... 
			GL.MatrixMode(MatrixMode.Projection);
			GL.PushMatrix();
            GL.LoadIdentity();
			GL.Ortho(0, texture.Size.Width, 0, texture.Size.Height, -1000, 1000);
			
			// View port stuffs
			GL.PushAttrib((uint)AttribMask.ViewportBit);
			GL.Viewport(0, 0, texture.Size.Width, texture.Size.Height);
            
            // Clear with alpha 0f... that way the BG is nice and transparent :)
            GL.ClearColor(0f, 0f, 0f, 0f); // set clear color
            GL.Clear((uint)ClearBufferMask.ColorBufferBit); // perform clear op
            
        	// Render -- both using the 'real' render func and doing it ourselves works.
        	// Is there a need to use the render func, as this doubles the render time
        	// from 3ms to 7ms! Do we need context stuff? I don't understand it, and it works fine without... hm
        	//this.Render();
        	foreach (Renderable ren in r.Values)
            	ren.Render();
            	
            // In case the caller wants to perform anything here... like saving to a file, y'know
            if (useFBOFunc != null)
            	useFBOFunc();
      
        	// Cleanup Viewport & Ortho
        	GL.PopAttrib(); // return viewport info
        	GL.MatrixMode(MatrixMode.Projection);
     		GL.PopMatrix();
     		
     		// Unbind FBO 
        	GL.FramebufferTexture2D(GLEnum.Framebuffer, GLEnum.ColorAttachment0, GLEnum.Texture2D, 0, 0);
        	GL.BindFramebuffer(GLEnum.Framebuffer, 0);
        	GL.DrawBuffer(DrawBufferMode.Back);
	
			WindowInternal.ReleaseContext();
        	this.UnblockRendering();
      	}
	}
}
